package com.oschina.bottle.utils.validator;

import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;

/**
 * 校验身份证号码
 *
 * @author <a href="mailto:zhongchao@gegejia.com">zhong</a>
 * @version 1.0 2016/11/11
 * @since 1.0
 */
public class IDValidator {

    /**
     * 身份证号验证 1. 身份证号码结构 公民身份号码是特征组合码，由十七位数字本体码和一位校验码组成。排列顺序从左至右依次为：六位数字地址码，八位数字出生日期码，三位数字顺序码和一位数字校验码。 2. 地址码(前6位)
     * 表示编码对象常住户口所在县(市、旗、区)的行政区划代码，按GB/T2260的规定执行。 3. 出生日期码（第七位至十四位） 表示编码对象出生的年、月、日，按GB/T7408的规定执行，年、月、日代码之间不用分隔符。 4.
     * 顺序码（第十五位至十七位） 表示在同一地址码所标识的区域范围内，对同年、同月、同日出生的人编定的顺序号， 顺序码的奇数分配给男性，偶数分配给女性。 5. 校验码（第十八位数） （1）十七位数字本体码加权求和公式 S =
     * Sum(Ai * Wi), i = 0, ... , 16 ，先对前17位数字的权求和Ai:表示第i位置上的身份证号码数字值 Wi:表示第i位置上的加权因子 Wi: 7 9 10 5 8 4 2 1 6 3 7 9 10 5
     * 8 4 2 (2）计算模 Y = mod(S, 11) （3）通过模得到对应的校验码 Y: 0 1 2 3 4 5 6 7 8 9 10 校验码: 1 0 X 9 8 7 6 5 4 3 2
     */

    // wi =2(n-1)(mod 11)
    final static int[] wi = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2, 1};

    // verify digit
    final static int[] vi = {1, 0, 'X', 9, 8, 7, 6, 5, 4, 3, 2};

    //月份码
    private static Map<String, Integer> dateMap = new HashMap<>();

    static {
        dateMap.put("01", 31);
        dateMap.put("02", null);
        dateMap.put("03", 31);
        dateMap.put("04", 30);
        dateMap.put("05", 31);
        dateMap.put("06", 30);
        dateMap.put("07", 31);
        dateMap.put("08", 31);
        dateMap.put("09", 30);
        dateMap.put("10", 31);
        dateMap.put("11", 30);
        dateMap.put("12", 31);
    }

    //地区码
    private static Map<String, String> areaCodeMap = new HashMap<>();

    static {
        areaCodeMap.put("11", "北京");
        areaCodeMap.put("12", "天津");
        areaCodeMap.put("13", "河北");
        areaCodeMap.put("14", "山西");
        areaCodeMap.put("15", "内蒙古");
        areaCodeMap.put("21", "辽宁");
        areaCodeMap.put("22", "吉林");
        areaCodeMap.put("23", "黑龙江");
        areaCodeMap.put("31", "上海");
        areaCodeMap.put("32", "江苏");
        areaCodeMap.put("33", "浙江");
        areaCodeMap.put("34", "安徽");
        areaCodeMap.put("35", "福建");
        areaCodeMap.put("36", "江西");
        areaCodeMap.put("37", "山东");
        areaCodeMap.put("41", "河南");
        areaCodeMap.put("42", "湖北");
        areaCodeMap.put("43", "湖南");
        areaCodeMap.put("44", "广东");
        areaCodeMap.put("45", "广西");
        areaCodeMap.put("46", "海南");
        areaCodeMap.put("50", "重庆");
        areaCodeMap.put("51", "四川");
        areaCodeMap.put("52", "贵州");
        areaCodeMap.put("53", "云南");
        areaCodeMap.put("54", "西藏");
        areaCodeMap.put("61", "陕西");
        areaCodeMap.put("62", "甘肃");
        areaCodeMap.put("63", "青海");
        areaCodeMap.put("64", "宁夏");
        areaCodeMap.put("65", "新疆");
        areaCodeMap.put("71", "台湾");
        areaCodeMap.put("81", "香港");
        areaCodeMap.put("82", "澳门");
        areaCodeMap.put("91", "国外");
    }

    /**
     * 验证身份证号码是否符合规则
     */
    public static boolean verify(String idCard) {

        //1. 获取身份证长度, 便于计算
        int length = StringUtils.length(idCard);

        //2. 验证身份证长度(15或18位)
        if (length != 15 && length != 18) {
            return false;
        }

        //3. 验证身份证号码, 除了最后一位, 其他是否包含字母
        if (!containsAllNumber(StringUtils.substring(idCard, 0, length - 1))) {
            return false;
        }

        //4. 验证身份证的地区码
        if (!verifyAreaCode(StringUtils.substring(idCard, 0, 2))) {
            return false;
        }

        //5. 验证日期
        if (!verifyBirthdayCode(length == 15 ? String.format("19%s", StringUtils.substring(idCard, 6, 12))
            : StringUtils.substring(idCard, 6, 14))) {
            return false;
        }

        return length == 15 || verifyMOD(idCard);
    }

    /**
     * 判断传入的字符串,均为数字
     *
     * @param code 身份证的前n-1位
     * @return true/false
     */
    private static boolean containsAllNumber(String code) {
        return RegExpValidator.validate(RegExpValidator.NUM_REG, code);
    }

    /**
     * 验证地区码
     *
     * @param code 地区码
     */
    private static boolean verifyAreaCode(String code) {
        return areaCodeMap.containsKey(code);
    }

    /**
     * 身份证的日期码, 固定格式为(YYYYMMDD)
     */
    private static boolean verifyBirthdayCode(String code) {
        Integer year = Integer.valueOf(StringUtils.substring(code, 0, 4));

        //如果是1900年之前, 不做考虑
        if (year < 1900) {
            return false;
        }

        String month = StringUtils.substring(code, 4, 6);

        if (!dateMap.containsKey(month)) {
            return false;
        }

        String day = StringUtils.substring(code, 6, 8);
        Integer dayNum = dateMap.get(month);

        //获取2月的天数
        if (dayNum == null) {
            //2月的情况
            dayNum = year % 400 == 0 || (year % 4 == 0 && year % 100 != 0) ? 29 : 28;

            if (Integer.valueOf(day) > dayNum) {
                return false;
            }

        }

        if (Integer.valueOf(day) < 1 || Integer.valueOf(day) > dayNum) {
            return false;
        }

        return true;
    }

    /**
     * 18位身份证验证最后一位校验码
     *
     * @param idCard (18位身份证号码)
     */
    private static boolean verifyMOD(String idCard) {

        return StringUtils.upperCase(String.valueOf(idCard.charAt(17))).equals(getVerify(idCard));
    }

    /**
     * 获取校验码 只针对18位身份证
     */
    private static String getVerify(String idCard) {
        int sum = 0;
        for (int i = 0; i < 17; i++) {
            int k = Integer.valueOf(String.valueOf(idCard.charAt(i)));
            sum += wi[i] * k;
        }

        int remaining = sum % 11;

        return remaining == 2 ? "X" : String.valueOf(vi[remaining]);
    }
}
